// Styling
var ceduStyleNode;
(function() {
  var ceduStyleNode = document.createElement('STYLE');
  var rules = '.control_block { visibility: hidden; text-align: center; border-top: thin solid silver; border-bottom: thin solid silver }';
  rules += ' table.control { border-collapse: collapse; display: inline }';
  rules += ' td.control { font: 10px sans-serif; vertical-align: top; text-align: left; padding: 0 }';
  rules += ' td.control_block_title { padding-right: 1em; padding-top: 0.5em; font-size: 12px; }';
  rules += ' td.control_group { border-left: thin dotted silver; border-right: thin dotted silver }';
  rules += ' td.control_group_title { padding-left: 3px; border-bottom: thin dotted silver }';
  rules += ' td.control_option { font-style: italic }';
  rules += ' input[type=checkbox] { margin-top: 1px; margin-bottom: 1px; vertical-align: middle }';
  rules += ' .footnotes { font: 10px sans-serif }';
  ceduStyleNode.innerHTML = rules;
  document.getElementsByTagName('HEAD')[0].appendChild(ceduStyleNode);
})()

// Dimensions.
var wYr = 120,  // horizontal spacing per year
    wGr = wYr / 2,   // horizontal shift per grade level
    hCPI = 80,  // height of each CPI rule
    wBar = 5,   // width of CPI bar
    tickLength = 14,
    // Line angles, in radians clockwise from vertical.
    yearAngle = Math.atan(hCPI / wGr) + Math.PI / 2,
    cohortAngle = Math.atan((wYr - wGr) / hCPI);
    // Ranges as [min, max].
    rYrs =  [pv.min(table, function(r) { return r.year }), pv.max(table, function(r) { return r.year })]
    rGrs =  [pv.min(table, function(r) { return r.grade }), pv.max(table, function(r) { return r.grade })],
    // Gross width magnitudes.
    wYrs = wYr * (rYrs[1] - rYrs[0]),  // width to include all years
    wGrs = wGr * (rGrs[1] - rGrs[0]),  // maximum horizontal shift for grades

    margin = {left: 70, top:20, right:100, bottom: 55},
    
    animateSequence = {grade: 0, year: 0, gradYear: 0},
    animateDelay = 300,  // milliseconds between, eg, grade baselines [300]
    animateStageDelay = 1500;  // milliseconds betwee, eg, grades and years [1500]

function showGradeFence() {
  return document.getElementById('show_grade_fence_checkbox').checked;
}
function showYearFence() {
  return document.getElementById('show_year_fence_checkbox').checked;
}
function showClassFence() {
  return document.getElementById('show_class_fence_checkbox').checked;
}
function showStateLines() {
  return document.getElementById('show_state_lines_checkbox').checked;
}
function showGauges() {
  return document.getElementById('show_gauges_checkbox').checked;
}
function showValues() {
  return document.getElementById('show_gauges_checkbox').checked &&
         document.getElementById('show_values_checkbox').checked;
}
function showConfidenceIntervals() {
  return document.getElementById('show_gauges_checkbox').checked &&
         document.getElementById('show_confidence_intervals_checkbox').checked;
}
function showStateAverage() {
  return document.getElementById('show_gauges_checkbox').checked &&
         document.getElementById('show_state_average_checkbox').checked;
}
function showGradeBaseline() {
  return document.getElementById('show_grade_baselines_checkbox').checked;
}
function showYearBaseline() {
  return document.getElementById('show_year_baselines_checkbox').checked;
}
function showClassBaseline() {
  return document.getElementById('show_class_baselines_checkbox').checked;
}
function showClassLabels() {
  return document.getElementById('show_class_labels_checkbox').checked;
}
function show(what) {
  // Check the corresponding check box.
  var box = document.getElementById('show_' + what + '_checkbox');
  box && (box.checked = true);
}

function gradYear(yr, gr) { return yr + 12 - gr }
var rGrad = [gradYear(rYrs[0], rGrs[1]), gradYear(rYrs[1], rGrs[0])];
function gradeIn(yr, grad) { return yr + 12 - grad }  // back-calculate grade for a year
function inDataSet(yr, grad) {
  var gr = gradeIn(yr, grad);
  return (gr >= rGrs[0] && gr <= rGrs[1]) && (yr >= rYrs[0] && yr <= rYrs[1]);
}
function yrsCohort(grad) {
  // Return a list of years this cohort intersects with our data set.
  return range(rYrs).filter(function(yr) { return inDataSet(yr, grad) });
}

var w = wYrs + wGrs,
    h = hCPI * (rGrs[1] - rGrs[0] + 1);

var yrgr = pv.nest(table)
  .key(function(r) { return r.year  })
  .sortValues(function(r1, r2) { return r1.grade - r2.grade })
  ;
var gryr = pv.nest(table)
  .key(function(r) { return r.grade } )
  .sortValues(function(r1, r2) { return r1.year - r2.year })
  ;
var gradyr = pv.nest(table)
  .key(function(r) { return gradYear(r.year, r.grade) } )
  .sortValues(function(r1, r2) { return r1.year - r2.year })
  ;
var xYr = pv.Scale.linear()
  .domain(pv.min(table, function(r) { return r.year }), pv.max(table, function(r) { return r.year }))
  .range(0, wYrs)
  ;
var xGrShift = pv.Scale.linear()
  .domain(table, function(r) { return r.grade })
  .range(0, wGrs)
  ;
function xGr(gr) { return wGrs - xGrShift(gr) }
var yGr = pv.Scale.linear()
  .domain(rGrs[0], rGrs[1] + 1)
  .range(0, h)
  ;
var ycpi = pv.Scale.linear()
  .domain(0, 100)
  .range(0, hCPI)
  ;

// Subject codes
var subject = { 
  10: 'English',
  20: 'Math',
  30: 'Science',
  31: 'Biology',
  32: 'Chemistry',
  33: 'Introductory Physics',
  34: 'Technology/Engineering' };

// Really really rough, harebrained estimate of standard error in CPI measurement.
// Please don't kill me.
function stderr(subjectCode, cpi, n) {
  // Return estimated standard error in aggregate CPI value for a subject and given number of scores.
  // Construct a 95% *comparison* interval 1.4 stderr lengths below and above a district CPI value.
  // See Harvey Goldstein and Michael J. R. Healy
  //     Journal of the Royal Statistical Society. Series A (Statistics in Society)
  //     Vol. 158, No. 1 (1995), pp. 175-177 
  //     http://www.jstor.org/pss/2983411
  // Typical raw point standard errors by subject code:
  var stderrRaw = {10: 2.7, 20: 3.5, 30: 3.5, 31: 3.3, 32: 3.8, 33: 3.6, 34: 3.6};
  // In the most populous score ranges, each raw point value typically maps to a scaled score step (2 points).
  var stderrSS = stderrRaw[subjectCode] * 1.8;
  // Scaled scores 200-240 correspond to 0-100 CPI points.
  var stderrCPIind = stderrSS / 40 * 100;  // estimated standard error for an individual CPI measurement
  // Assume the CPI value given is 1/3 way between %P and 100.
  // percP + 3(cpi - percP) = 100
  // 3cpi - 2percP = 100
  // percP = (100 - 3cpi) / (-2)
  var percP = (100 - 3 * cpi) / (-2);
  // Assume 80% of proficient students don't contribute to CPI variance.
  var nbeyondP = 0.8 * percP / 100 * n;
  // nbeyondP / n of the CPI value has a standard error of about 0.
  // Weight the effect of the remaining occurrences.
  var nrem = n - nbeyondP;
  var stderrCPIrem = stderrCPIind / Math.sqrt(nrem);  // standard error in the remaining occurrences
  var stderrCPIagg = stderrCPIrem * nrem / n;  // standard error of the aggregate measure
  return stderrCPIagg;
}

function cpiTipText(r) {
  var stdErr = stderr(subjectCode, r.cpi, r.n);
  var confInt = [(r.cpi - 1.4 * stdErr).toFixed(1), (r.cpi + 1.4 * stdErr).toFixed(1)];
  var text = 'Grade ' + r.grade + ', ' + r.year + ', ' + r.n + ' students, CPI = ' + r.cpi.toFixed(1);
  text += '  Comparison 95% confidence interval = (' + confInt[0] + ', ' + confInt[1] + ')';
  text += '  State CPI for this test = ' + r.statecpi.toFixed(1);
  return text;
}

var lightGray = pv.color('#B0B0B0'),
    darkGray = pv.color('#303030'),
    lightOrange = pv.color('rgba(255, 187, 120, 0.4)'),
    lighterOrange = pv.color('rgba(255, 222, 191, 0.4)'),
    solidOrange = pv.color('rgb(255, 150, 46)'),
    brown = pv.color('#673923'),
    brown2 = pv.color('rgb(177, 98, 60)'),
    stateBlue = pv.color('#004386'),
    lightStateBlue = pv.color('#5E87B1'),
    hotPink = pv.color('rgb(255, 0, 192)');

var vis = new pv.Panel()
  .canvas('cedu_vis')
  .width(w)
  .height(h)
  .left(margin.left)
  .right(margin.right)
  .top(margin.top)
  .bottom(margin.bottom)
  ;

// Trace a light grid on the floor, intersecting at grade,year points.
// First, grades.
var gradeBaselines = vis.add(pv.Panel)
  .data(range(rGrs))  // [g0, g1, .., gn]
  .visible(function(gr) { return gr <= animateSequence.grade })
  ;
gradeBaselines.add(pv.Line)
    .data(rYrs)  // [first, last]
    .visible(showGradeBaseline)
    .left(function(yr, gr) { return xGr(gr) + xYr(yr) })
    .bottom(function(yr, gr) { return yGr(gr) })
    .strokeStyle(lightGray)
    .lineWidth(1.5)
  .parent
    .add(pv.Rule)
    .top(null)
    .bottom(yGr)
    .left(function(gr) { return xGr(gr) - tickLength })
    .width(tickLength)
    .strokeStyle(lightGray)
    .lineWidth(1.5)
      .anchor('left')
      .add(pv.Label)
      .text(function(gr) { return 'grade ' + gr })
      .font('12px sans-serif')
      .textStyle(darkGray)
   ;
// Second, years.
yearBaselines = vis.add(pv.Panel)
  .data(range(rYrs))  // [y0, y1, .., yn]
  .visible(function(yr) { return yr <= animateSequence.year })
  ;
yearBaselines.add(pv.Line)
    .data(rGrs)  // [first, last]
    .visible(showYearBaseline)
    .left(function(gr, yr) { return xGr(gr) + xYr(yr) })
    .bottom(function(gr, yr) { return yGr(gr) })
    .strokeStyle(lightGray)
  .parent
    .add(pv.Dot)
    .top(null)
    .bottom(0)
    .left(function(yr) { return xGr(rGrs[0]) + xYr(yr) })
    .shape('tick')
    .strokeStyle(lightGray)
    .lineWidth(1.5)
    .size(tickLength)
    .angle(yearAngle)
      .add(pv.Label)
      .textAngle(yearAngle - Math.PI / 2)
      .textBaseline('middle')
      .font('12px sans-serif')
      .textStyle(darkGray)
      .top(null)
      .left(function(yr) { return xGr(rGrs[0]) + xYr(yr) + Math.cos(yearAngle - Math.PI / 2) * tickLength })
      .bottom(-(Math.sin(yearAngle - Math.PI / 2) * tickLength))
  ;
// Third, class cohorts.
classBaselines = vis.add(pv.Panel)
  .data(range(rGrad))  // [gradyr0, gradyr1, .., gradyrn]
  .visible(function(grad) { return grad <= animateSequence.gradYear })
  ;
classBaselines.add(pv.Line)
    .data(yrsCohort)
    .top(null)
    .left(function(yr, grad) { return xGr(gradeIn(yr, grad)) + xYr(yr) })
    .bottom(function(yr, grad) { return yGr(gradeIn(yr, grad)) })
    .lineWidth(0.5)
    .strokeStyle(function() { return showClassBaseline() ? lightGray : 'white' })
      .add(pv.Dot)
      .visible(function(yr, grad) { return gradeIn(yr, grad) == rGrs[1] || yr == rYrs[1] })
      .shape('tick')
      .lineWidth(0.5)
      .size(tickLength)
      .angle(cohortAngle)
        .add(pv.Dot)
        .visible(function() { return this.proto.visible() && showClassLabels() })
        .shape('triangle')
        .angle(cohortAngle + Math.PI)
        .size(4)
        .fillStyle(lightGray)
        .left(function(yr, grad) { return xGr(gradeIn(yr, grad)) + xYr(yr) + Math.sin(cohortAngle) * tickLength })
        .bottom(function(yr, grad) { return yGr(gradeIn(yr, grad)) + Math.cos(cohortAngle) * tickLength })
          .add(pv.Label)
          .text(function(yr, grad) { return 'class of ' + grad })
          .textAngle(cohortAngle - Math.PI / 2)
          .textBaseline('middle')
          .font('12px sans-serif')
          .textStyle(darkGray)
  ;

// Display fences that highlight changing CPI values across dimensions.
// Across grades.
var gradeFence = vis.add(pv.Panel)
  .data(gryr.entries())
  .visible(showGradeFence)
  .left(function(e) { return xGr(e.key) })
  .bottom(function(e) { return yGr(e.key) })
  ;
gradeFence.add(pv.Area)
  .data(function(e) { return e.values })
  .left(function(r) { return xYr(r.year) })
  .bottom(0)
  .height(function(r) { return ycpi(r.cpi) })
  .lineWidth(1)
  .strokeStyle('lightgray')
  .fillStyle(lightOrange)
  .title(function(r, e) { return 'Grade ' + r.grade + ':  ' + e.values.map(function(ri) { return ri.cpi.toFixed() }).join('  ') })
    .add(pv.Area)
    .bottom(function(r) { return ycpi(r.cpi) })
    .height(function(r) { return hCPI - ycpi(r.cpi) })
    .fillStyle(lighterOrange)
  ;
gradeFence.add(pv.Line)
  .data(function(e) { return e.values })
  .visible(showStateLines)
  .left(function(r) { return xYr(r.year) })
  .bottom(function(r) { return ycpi(r.statecpi) })
  .strokeStyle(lightStateBlue)
  ;

// Across years.
var yearFence = vis.add(pv.Panel)
  .data(yrgr.entries())
  .visible(showYearFence)
  .left(function(e) { return xYr(e.key) })
  .bottom(0)
  ;
yearFence.add(pv.Area)
  .data(function(e) { return e.values })
  .left(function(r) { return xGr(r.grade) })
  .bottom(function(r) { return yGr(r.grade) })
  .height(function(r) { return ycpi(r.cpi) })
  .strokeStyle('lightgray')
  .fillStyle(lightOrange)
  .title(function(r, e) { return r.year + ' grade ' + e.values.map(function(ri) { return ri.grade + ':' + ri.cpi.toFixed() }).join('  ') })
    .add(pv.Area)
    .bottom(function(r) { return yGr(r.grade) + ycpi(r.cpi) })
    .height(function(r) { return hCPI - ycpi(r.cpi) })
    .fillStyle(lighterOrange)
  ;
yearFence.add(pv.Line)
  .data(function(e) { return e.values })
  .visible(showStateLines)
  .left(function(r) { return xGr(r.grade) })
  .bottom(function(r) { return yGr(r.grade) + ycpi(r.statecpi) })
  .strokeStyle(lightStateBlue)
  ;

// Across class cohorts.
var classFence = vis.add(pv.Panel)
  .data(gradyr.entries())
  .visible(function(e) { return showClassFence() && e.values.length > 1 });
  ;
classFence.add(pv.Area)
  .data(function(e) { return e.values })
  .left(function(r) { return xGr(r.grade) + xYr(r.year) })
  .bottom(function(r) { return yGr(r.grade) })
  .height(function(r) { return ycpi(r.cpi) })
  .strokeStyle('lightgray')
  .fillStyle(lightOrange)
  .title(function(r, e) { return 'Class of ' + e.key + ', by grade: ' + e.values.map(function(ri) { return ri.grade + ':' + ri.cpi.toFixed() }).join('  ') })
    .add(pv.Area)
    .bottom(function(r) { return yGr(r.grade) + ycpi(r.cpi) })
    .height(function(r) { return hCPI - ycpi(r.cpi) })
    .fillStyle(lighterOrange)
  ;
classFence.add(pv.Line)
  .data(function(e) { return e.values })
  .visible(showStateLines)
  .left(function(r) { return xGr(r.grade) + xYr(r.year) })
  .bottom(function(r) { return yGr(r.grade) + ycpi(r.statecpi) })
  .strokeStyle(lightStateBlue)
  ;

// Draw a CPI thermometer gauge for each test, at intersections of grade, year, and class cohort lines.
var gauges = vis.add(pv.Bar)
  .data(table)
  .visible(showGauges)
  .left(function(r) { return xGr(r.grade) + xYr(r.year) - wBar / 2 })
  .width(wBar)
  .bottom(function(r) { return yGr(r.grade) })
  .height(hCPI)
  .fillStyle('white')
  .strokeStyle('gray')
  .title(cpiTipText)
  ;
gauges.add(pv.Bar)
  .fillStyle('silver')
  .height(function(r) { return ycpi(r.cpi) })
  ;
gauges.anchor('top')
  .add(pv.Label)
  .visible(showValues)
  .text(function(r) { return r.cpi.toFixed() })
  .textBaseline('bottom')
  .font('12px sans-serif')
  ;
gauges.add(pv.Rule)
  .visible(showStateAverage)
  .lineWidth(3)
  .strokeStyle(lightStateBlue)
  .bottom(function(r) { return yGr(r.grade) + ycpi(r.statecpi) })
  .height(null)
  .left(function(r) { return xGr(r.grade) + xYr(r.year) - wBar / 2 - 3 })
  .width(3)
    .add(pv.Rule)
    .left(function(r) { return xGr(r.grade) + xYr(r.year) + wBar / 2 })
  ;
gauges.add(pv.Bar)
  .visible(showConfidenceIntervals)
  .strokeStyle(hotPink)
  .lineWidth(0.5)
  .fillStyle(pv.color('transparent'))
  .left(function() { return this.proto.left() - 2 })
  .width(function() { return this.proto.width() + 4 })
  .bottom(function(r) { return yGr(r.grade) + ycpi(r.cpi) - ycpi(1.4 * stderr(subjectCode, r.cpi, r.n)) })
  .height(function(r) { return ycpi(2.8 * stderr(subjectCode, r.cpi, r.n)) })
  ;

function redraw() {
  vis.render();
}

// Introduce the visualization one dimension at a time.
function introduce() {
  animateSequence.grade = 0;
  animateSequence.year = 0;
  animateSequence.gradYear = 0;
  vis.render();
  setTimeout(introGrades, animateStageDelay);
}
function introGrades() {
  if (animateSequence.grade == 0) animateSequence.grade = rGrs[0] - 1;
  if (animateSequence.grade < rGrs[1]) {
    animateSequence.grade++;
    gradeBaselines.render();
    setTimeout(introGrades, animateDelay);
  } else {
    setTimeout(introYears, animateStageDelay);
  }
}
function introYears() {
  if (animateSequence.year == 0) animateSequence.year = rYrs[0] - 1;
  if (animateSequence.year < rYrs[1]) {
    animateSequence.year++;
    yearBaselines.render();
    setTimeout(introYears, animateDelay);
  } else {
    setTimeout(introClasses, animateStageDelay);
  }
}
function introClasses() {
  if (animateSequence.gradYear == 0) animateSequence.gradYear = rGrad[0] - 1;
  if (animateSequence.gradYear < rGrad[1]) {
    animateSequence.gradYear++;
    classBaselines.render();
    setTimeout(introClasses, animateDelay);
  } else {
    setTimeout(introColumns, animateStageDelay);
  }
}
function introColumns() {
  show('gauges');
  redraw();
  setTimeout(introGradeFence, animateStageDelay);
}
function introGradeFence() {
  show('grade_fence');
  redraw();
  setTimeout(introControls, animateStageDelay);
}
function introControls() {
  document.getElementById("control_block").style.setProperty('visibility', 'visible', '');
}

function range(r) {
  // Return a sequence from a to b, inclusive,
  // from duple argument [a, b].
  return pv.range(r[0], r[1] + 1);
}

introduce();
